Troubled markets

An exploration on Argentina's ADRs recent performance

American Depositary Reciepts (ADRs) are financial instruments that allow US investors to purchase stocks in foreign companies. Argentina has a number of companies listed in US exchanges through ADRs.
In this notebook, we will explore the performance of these ADRs with a focus on the recent months, where political uncertainty sent markets into turmoil: the country’s Merval stock index fell 48% in dollar terms in a single day, the second-largest one-day drop in any of the 94 markets tracked by Bloomberg since 1950[1], causing a 20% devaluation of the Argentine peso and a sharp drop in bond prices.
We will be looking at the end-of-day data of the Argentine ADRs, and their corresponding put and call options.

Data munging

data_dir = 'data/'
tickers = [
    'BMA', 'BFR', 'CEPU', 'CRESY', 'EDN', 'GGAL', 'SUPV', 'IRS',
    'IRCP', 'LOMA', 'NTL', 'MELI', 'PAM', 'PZE', 'TEO', 'TS', 'TX',
    'TGS', 'YPF'
]

dfs = [pd.read_csv(os.path.join(data_dir, ticker.lower() + '.csv')) for ticker in tickers]

adrs_df = pd.concat(dfs, axis=0, ignore_index=False)
adrs_df.to_csv(os.path.join(data_dir, 'adrs.csv'), index=False)

Exploration

We begin our exploration of the end-of-day (EOD) data for Argentina's ADRs.

adrs_df = pd.read_csv(os.path.join(data_dir, 'adrs.csv'), index_col='date', parse_dates=['date'])
adrs_df.head()
symbol close high low open volume adjClose adjHigh adjLow adjOpen adjVolume divCash splitFactor
date
2006-03-27 00:00:00+00:00 BMA 23.05 23.05 22.23 22.89 1065200 15.524521 15.524521 14.972239 15.416759 1065200 0.0 1.0
2006-03-28 00:00:00+00:00 BMA 22.38 22.47 21.90 22.47 1556100 15.073266 15.133883 14.749979 15.133883 1556100 0.0 1.0
2006-03-29 00:00:00+00:00 BMA 22.84 23.14 22.05 22.10 641300 15.383083 15.585138 14.851006 14.884682 641300 0.0 1.0
2006-03-30 00:00:00+00:00 BMA 22.75 23.10 22.70 23.00 293600 15.322467 15.558197 15.288791 15.490846 293600 0.0 1.0
2006-03-31 00:00:00+00:00 BMA 22.93 22.93 22.35 22.83 113600 15.443700 15.443700 15.053061 15.376348 113600 0.0 1.0

The data is indexed by date, with the symbol column holding the ticker name, and columns for the open and close prices (the price of the symbol at the start/end of the market day) and the high and low prices seen for the symbol at that date.
We'll begin plotting the adjusted close prices for each symbol (price adjusted for dividends payed and stock splits). The reset of the columns can be safely ingored for our purposes.

adrs_df.groupby('symbol')['adjClose'].describe()
count mean std min 25% 50% 75% max
symbol
BFR 4936.0 8.279068 5.706390 0.744824 3.953318 5.724786 12.029480 25.454491
BMA 3371.0 35.178278 25.470378 4.999717 15.792980 24.600919 48.906183 125.424911
CEPU 386.0 10.988264 2.897472 3.460000 9.060000 9.975000 12.277500 17.919462
CRESY 4936.0 9.937665 3.951959 2.923092 6.744933 9.466466 12.010526 21.638971
EDN 3099.0 15.310731 12.471194 1.710000 6.330000 12.110000 20.245000 62.550000
GGAL 4795.0 13.551952 13.195641 0.211733 5.404357 7.966350 16.436948 71.458310
IRCP 3971.0 15.634198 14.424214 1.293231 4.377059 10.018225 21.425000 62.224671
IRS 4936.0 10.068860 5.624760 1.904546 6.130128 8.541985 13.279294 32.170000
LOMA 449.0 14.069621 5.356757 5.400000 10.340000 11.740000 21.120000 25.020000
MELI 3025.0 138.089838 126.898120 8.023763 59.112264 93.592940 153.612638 690.100000
NTL 4519.0 13.146904 8.402332 0.392764 6.546068 12.756651 18.681158 51.700000
PAM 2479.0 21.467967 18.064011 2.850000 9.885292 14.590000 30.800000 71.650000
PZE 4608.0 5.865744 2.463131 1.515041 4.362068 5.365770 6.720500 14.550000
SUPV 816.0 15.182065 7.337212 3.160000 8.918750 13.907507 17.840770 32.369630
TEO 4936.0 12.194805 6.370887 0.380288 7.642209 12.427119 16.013961 35.963224
TGS 4936.0 4.000758 4.341288 0.287635 1.615702 2.457407 3.514175 20.523048
TS 4195.0 25.943267 10.946160 2.261653 21.353160 28.215679 34.121053 55.724628
TX 3408.0 20.250514 6.297409 3.256623 16.042294 19.848379 24.567725 39.303918
YPF 4936.0 21.578362 9.696556 3.164889 14.031595 21.594239 29.665596 47.309175

Daily returns

Next we'll calculate the daily returns.

$$R_n \equiv \frac{S_n - S_{n-1}}{S_{n-1}} \%$$
adrs_df['return'] = adrs_df.groupby('symbol')['adjClose'].pct_change() * 100
adrs_df.head()
symbol close high low open volume adjClose adjHigh adjLow adjOpen adjVolume divCash splitFactor return
date
2006-03-27 00:00:00+00:00 BMA 23.05 23.05 22.23 22.89 1065200 15.524521 15.524521 14.972239 15.416759 1065200 0.0 1.0 NaN
2006-03-28 00:00:00+00:00 BMA 22.38 22.47 21.90 22.47 1556100 15.073266 15.133883 14.749979 15.133883 1556100 0.0 1.0 -2.906725
2006-03-29 00:00:00+00:00 BMA 22.84 23.14 22.05 22.10 641300 15.383083 15.585138 14.851006 14.884682 641300 0.0 1.0 2.055407
2006-03-30 00:00:00+00:00 BMA 22.75 23.10 22.70 23.00 293600 15.322467 15.558197 15.288791 15.490846 293600 0.0 1.0 -0.394046
2006-03-31 00:00:00+00:00 BMA 22.93 22.93 22.35 22.83 113600 15.443700 15.443700 15.053061 15.376348 113600 0.0 1.0 0.791209
adrs_df.groupby('symbol')['return'].describe()
count mean std min 25% 50% 75% max
symbol
BFR 4935.0 0.053087 3.671496 -55.850622 -1.734230 0.000000 1.686544 46.760563
BMA 3370.0 0.083384 3.282690 -52.667364 -1.489327 0.000000 1.647746 27.008149
CEPU 385.0 -0.265245 4.306204 -55.915179 -1.913876 -0.317460 1.581028 16.880093
CRESY 4935.0 0.038327 2.767597 -38.090452 -1.250890 0.000000 1.180099 27.118644
EDN 3098.0 0.051126 3.888776 -58.983957 -1.690714 -0.031217 1.629187 27.551020
GGAL 4794.0 0.094611 4.627147 -56.117370 -1.626710 0.000000 1.693722 153.623188
IRCP 3970.0 0.142312 4.226301 -32.424983 -0.681957 0.000000 0.849814 36.986301
IRS 4935.0 0.015710 2.675223 -38.287402 -1.250000 0.000000 1.214575 18.083573
LOMA 448.0 -0.166143 4.533969 -57.298137 -1.875441 -0.087351 1.505056 22.650602
MELI 3024.0 0.165014 3.566247 -21.198668 -1.400264 0.083916 1.589953 36.000000
NTL 4518.0 0.082807 3.342394 -46.188341 -1.315789 0.000000 1.365299 30.000000
PAM 2478.0 0.060250 2.964323 -53.815490 -1.415248 0.000000 1.411089 16.941990
PZE 4607.0 0.066024 3.908461 -19.221411 -1.457769 0.000000 1.415805 179.843750
SUPV 815.0 -0.025345 4.183604 -58.746736 -1.407739 0.000000 1.494714 28.330206
TEO 4935.0 0.035329 3.079372 -33.375796 -1.419974 0.000000 1.450779 23.076923
TGS 4935.0 0.077615 3.385825 -48.035488 -1.494490 0.000000 1.615534 25.203252
TS 4194.0 0.084939 2.537497 -21.309735 -1.177830 0.113344 1.351878 21.576763
TX 3407.0 0.047116 3.028683 -19.678519 -1.351580 0.036819 1.413508 49.096099
YPF 4935.0 0.033014 2.563941 -34.052758 -1.121233 0.000000 1.123280 37.254597

Let's plot a histogram of the daily returns for each symbol.

We see most returns cluster around 0, with a few outliers. We can visualize them using a boxenplot.

We can filter the days with 30% or larger movement in prices (either up or down).

Now if we remove outliers, say discard days where return was higher than 10% or lower than -10%:

In finance, it is common to look at the log returns of an asset. Stock prices are assumed to follow a log-normal distribution, hence we should expect log returns to be distributed normally.

$$ln(S_T)\sim N\big[ln(S_0)+(\mu-\frac{\sigma^2}{2})T,\;\sigma^2T\big] \\ ln(\frac{S_T}{S_0})\sim N\big[(\mu-\frac{\sigma^2}{2})T, \;\sigma^2T\big]$$

Where $S_T$ is the price of the underlying at time $T$.
For a more detailed discussion on the assumptions of the Black-Scholes-Merton model, see chapter 15 of Options, Futures and Other Derivatives (9th Ed) by John Hull.
You can read more on the distribution of prices and returns here.

adrs_df['log_return'] = np.log(adrs_df['return'] / 100 + 1.)

Now we can calculate the volatility) $\sigma$ for each symbol, defined as the standard deviation of log returns.
As a comparison, we'll add the daily volatility (from 2000 to 2018) for the S&P 500 index, a stock market index that measures the stock performance of the 500 largest publicly traded companies in the United States.

spx_df = pd.read_csv(os.path.join(data_dir, 'spx_2000-2018.csv'),
                     index_col='date',
                     parse_dates=['date'])
spx_df['log_return'] = np.log(spx_df['price'] / spx_df['price'].shift(1))
adr_volatility = adrs_df.groupby('symbol')['log_return'].std()
adr_volatility['SPX'] = spx_df['log_return'].std()

sns.barplot(x=adr_volatility.index, y=adr_volatility.values).set_title(
    'Daily volatility $\sigma_{daily}$ for each symbol (std of log returns)',
    size=16);

Let's plot the mean yearly returns for each symbol. Again, we'll add the mean daily return of \$SPX as a benchmark.

A closer look at outliers

Let's plot the returns that are $3\sigma$ away from the mean. If log returns were truly distributed normally, then we should expect $99.7\%$ of them to lie in the interval $(\mu_R - 3\sigma \mu_R, \mu_R + 3\sigma \mu_R)$.

def outlier_filter(symbol_df):
    symbol = symbol_df['symbol'].iloc[0]
    return symbol_df.loc[symbol_df['log_return'].abs() >= 3 * adr_volatility[symbol]]

outliers = adrs_df.groupby('symbol').apply(outlier_filter).reset_index(level=0, drop=True)
sns.scatterplot(x=outliers.index, y='log_return', hue='symbol',
                data=outliers).set_title('$3\sigma$ outlier daily returns',
                                         size=16);

We see a large number of outlier return days. To put that in perspective, let's calculate the proportion of outlier returns for each symbol in the data, that is the number of days where $3\sigma$ returns where observed over the total number of observations.

Finally, we'll look at the cumulative log returns over time.

pivoted = adrs_df.pivot(columns='symbol', values='log_return')
pivoted.cumsum().apply(np.exp).plot(title='Cumulative log returns');

ADR options and the volatility smile

Now we'll examine the options end-of-day data for the ADRs. Options are derivative contracts based on an underlying asset such as stocks. They offer the buyer the opportunity to buy or sell the underlying asset at a given price (strike price). You can find more information on options in this notebook.

underlying underlying_last exchange optionroot type expiration strike last net bid ask volume openinterest impliedvol delta gamma
quotedate
2019-07-03 TEO 17.79 CBOE TEO190719C00002500 call 2019-07-19 2.5 0.0 0.0 13.0 17.8 0 0 0.0200 1.0000 0.0000
2019-07-03 TEO 17.79 CBOE TEO190719C00005000 call 2019-07-19 5.0 0.0 0.0 10.5 15.2 0 0 6.3229 0.9446 0.0045
2019-07-03 TEO 17.79 CBOE TEO190719C00007500 call 2019-07-19 7.5 0.0 0.0 8.0 12.8 0 0 3.5717 0.9336 0.0093
2019-07-03 TEO 17.79 CBOE TEO190719C00010000 call 2019-07-19 10.0 0.0 0.0 5.5 10.2 0 0 2.0354 0.9375 0.0156
2019-07-03 TEO 17.79 CBOE TEO190719C00012500 call 2019-07-19 12.5 0.0 0.0 3.0 7.8 0 199 1.2856 0.9215 0.0297

The options data is also indexed by date. These are the most important columns:

  • underlying: The ticker of the underlying asset.
  • underlying_last: The last quoted price of the underlying asset.
  • optionroot: The name of the contract.
  • type: Contract type (put or call)
  • strike: The price at which owner can execute (buy/sell underlying).
  • expiration: Date of expiration of the option.
  • bid: The price at which investor can sell this contract.
  • ask: The price at which investor can buy this contract.
  • openinterest: The total number of contract outstanding.
  • impliedvol: Volatility of the underlying as implied by the option price (according to BSM model)

Let's plot the volatility smile for each symbol at 2019-08-09.
The volatility smile plots the implied volatility (IV, a measure of the volatility of an underlying security as implied by the option prices) at the different strike levels.
We'll plot the IV for puts and calls for each symbol. The dashed line represents the spot price.

Now let's try the the same plot for the following Monday (2019-08-12). That day, the MERVAL (an index that tracks the biggest companies listed in the Buenos Aires Stock Exchange) crashed and lost close to 50% of its USD value.

Option price evolution

Next, we'll analyse how option prices changed during the month of August, 2019. Let's find the 10 most actively traded options (those with the highest open interest) for each symbol at the start of the month.

august_options = adr_options.loc['2019-08']
def filter_active(symbol_df, option_type='call'):
    return symbol_df.loc[symbol_df['type'] == option_type].nlargest(
        n=10, columns='openinterest')

month_start_date = august_options.index.min()
first_trading_day = august_options.loc[month_start_date]
most_active_calls = first_trading_day.groupby('underlying').apply(
    filter_active).reset_index(level=0, drop=True)
most_active_puts = first_trading_day.groupby('underlying').apply(
    lambda df: filter_active(df, 'put')).reset_index(level=0, drop=True)
call_contracts = most_active_calls['optionroot']
put_contracts = most_active_puts['optionroot']
august_active_calls = august_options.loc[august_options['optionroot'].isin(call_contracts)]
august_active_puts = august_options.loc[august_options['optionroot'].isin(put_contracts)]

august_active_calls['day'] = august_active_calls.index.strftime('%d')
august_active_puts['day'] = august_active_puts.index.strftime('%d')

August 2019

We'll plot the evolution of the ask price for the actively traded calls through August 2019.

June 2019

As a comparison, let's try plotting the option prices for June 2019.

june_options = adr_options.loc['2019-06']

October 2015

october_2015_options = pd.read_csv(os.path.join(data_dir, 'adr_options_October_2015.csv'),
                                   index_col='quotedate',
                                   parse_dates=['quotedate', 'expiration'])

November 2015

november_2015_options = pd.read_csv(os.path.join(data_dir, 'adr_options_november_2015.csv'),
                                   index_col='quotedate',
                                   parse_dates=['quotedate', 'expiration'])

December 2015

december_2015_options = pd.read_csv(os.path.join(data_dir, 'adr_options_December_2015.csv'),
                                   index_col='quotedate',
                                   parse_dates=['quotedate', 'expiration'])

January 2008

We'll examine the options data for January 2008, the beginning of the mortgage crisis. We only have data for 3 companies: \$MELI, \\$TS and \$TX

january_2008_options = pd.read_csv(os.path.join(data_dir, 'adr_options_january_2008.csv'),
                                   index_col='quotedate',
                                   parse_dates=['quotedate', 'expiration'])

Final notes

Countries such as Argentina exhibit high political beta: stocks show extreme sensitivity to political events. The MERVAL index nearly quadrupled its value in Pesos since December 2015, only to drop by %40 in a single day in August 2019.
Investors tend to exacerbate bull runs, discounting future growth in the current stock prices. At the prospect of regulatory changes and political turnover, they panic sell and seek less risky assets. There remains to be seen whether savvy traders can exploit both the overconfident bulls and the panicking bears, buying volatility from the former to sell it at a profit to the latter.